﻿// HSS.Interlink.Web.BaseDownloadHandler.cs
// ----------------------------------------------------------------------------
// Licensed under the MIT license (http://www.opensource.org/licenses/mit-license.html)
// ----------------------------------------------------------------------------
// HighSpeed-Solutions, LLC
// Copyright (c) 2001-2012
// ----------------------------------------------------------------------------
// File:       BaseDownloadHandler.cs
// Author:     HSS\gbanta
// Created:    07/16/2011
// Modified:   12/19/2011
// ----------------------------------------------------------------------------
namespace HSS.Interlink.Web
{
	#region Using Directives
	using System;
	using System.IO;
	using System.Web;
	using System.Net;
	#endregion

	#region BaseDownloadHandler
	/// <summary>
	/// BaseDownloadHandler is the base implementation of a File download
	/// processor and handles the Protocol portion of a download request.
	/// Implementors of this class should override the actual ProcessX methods.
	/// </summary>
	public abstract class BaseDownloadHandler : BaseHandler, IDownloadHandler
	{
		#region Constructors
		/// <summary>
		/// Constructor
		/// </summary>
		public BaseDownloadHandler()
		{
		}
		#endregion

		#region Properties
		/// <summary>
		/// Gets or sets if client side caching is enabled. Default: False
		/// </summary>
		public bool EnableClientCache { get; set; }
		/// <summary>
		/// Gets or sets the client side cache expiration. Default: 10 Minutes
		/// </summary>
		public TimeSpan ClientCacheExpiration
		{
			get { return _clientCacheExpiration; }
			set { _clientCacheExpiration = value; }
		} TimeSpan _clientCacheExpiration = new TimeSpan(0, 10, 0);
		/// <summary>
		/// Gets or sets the File Cacheability. Default: Public - response is cacheable by clients and shared (proxy) caches.
		/// </summary>
		/// <exception cref="ArgumentOutOfRangeException">Cannot set server-side caching when configuring Client Cacheability.</exception>
		public HttpCacheability ClientCacheability
		{
			get { return _clientCacheability; }
			set
			{
				if (value == HttpCacheability.Server ||
					value == HttpCacheability.ServerAndNoCache ||
					value == HttpCacheability.ServerAndPrivate)
				{
					throw new ArgumentOutOfRangeException("value", "Cannot set server-side caching when configuring Client Cacheability.");
				}
				_clientCacheability = value;
			}
		} HttpCacheability _clientCacheability = HttpCacheability.Public;
		/// <summary>
		/// Gets the array of one or more names or ids or keys of the requested files to download.
		/// </summary>
		public string[] RequestedFiles
		{
			get;
			private set;
		}
		/// <summary>
		/// Gets the name or id or key of the requested file to download.
		/// </summary>
		public string RequestedFile
		{
			get
			{
				return RequestedFiles[0];
			}
		}
		#endregion

		#region Methods

		#region IDownloadHandler
		/// <summary>
		/// Processes the actual download request. This method is used by HSS.Interlink and not meant to be called from user code.
		/// </summary>
		/// <param name="context">The current HttpContext</param>
		/// <param name="fileQuery">The currently parsed QueryString</param>
		void IDownloadHandler.Download(HttpContext context, FileQuery fileQuery)
		{
			var status = HttpStatusCode.InternalServerError;
			var jobFile = string.Empty;
			try
			{
				#region Process Open requests separate from download requests
				if (fileQuery.Command == FileQueryCommand.OpenFileInline || fileQuery.Command == FileQueryCommand.OpenFileAsAttachment)
				{
					context.Request.InputStream.Close();
					context.Response.Clear();
					context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
					return;
				}
				#endregion

				jobFile = this.CommonInit(context, fileQuery);

				switch (context.Request.HttpMethod.ToUpperInvariant())
				{
					case "POST":
						#region POST commands
						switch (this.Query.Command)
						{
							case FileQueryCommand.Prepare:
								#region Prepare
								DownloadTempCache.Current.Add(jobFile, context.Request);
								context.Request.InputStream.Close();
								context.Response.Clear();
								return;
								#endregion

							default:
								#region Default is failure
								context.Request.InputStream.Close();
								context.Response.Clear();
								context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
								return;
								#endregion
						}
						#endregion

					case "GET":
						#region GET commands
						switch (this.Query.Command)
						{
							case FileQueryCommand.GetSingleFile:
								#region Download SingleFile
								if (!this.PrepareJob(jobFile, true))
									return;
								status = this.GetSingleFile();
								if (status != HttpStatusCode.OK)
								{
									context.Response.Clear();
									context.Response.StatusCode = (int)status;
								}
								return;
								#endregion

							case FileQueryCommand.GetMultipleFiles:
								#region Download MultipleFiles
								if (!this.PrepareJob(jobFile, false))
									return;
								status = this.GetMultipleFiles();
								if (status != HttpStatusCode.OK)
								{
									context.Response.Clear();
									context.Response.StatusCode = (int)status;
								}
								return;
								#endregion

							default:
								#region Default is failure
								context.Request.InputStream.Close();
								context.Response.Clear();
								context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
								return;
								#endregion
						}
						#endregion

					default:
						#region No other VERBS supported
						context.Request.InputStream.Close();
						context.Response.Clear();
						context.Response.StatusCode = (int)HttpStatusCode.MethodNotAllowed;
						return;
						#endregion
				}
			}
			catch (Exception ex)
			{
				#region Handle Exceptions

				try { status = OnError(ex); }
				catch { status = HttpStatusCode.InternalServerError; }

				context.Response.Clear();
				context.Response.StatusCode = (int)status;

				#endregion
			}
		}
		/// <summary>
		/// Perform the open inline request. This method is used by HSS.Interlink and not meant to be called from user code.
		/// </summary>
		/// <param name="context">The current HttpContext</param>
		/// <param name="fileQuery">The currently parsed QueryString</param>
		void IDownloadHandler.OpenInline(System.Web.HttpContext context, FileQuery fileQuery)
		{
			var status = HttpStatusCode.InternalServerError;
			try
			{
				this.CommonInit(context, fileQuery);
				context.Request.InputStream.Close();
				this.RequestedFiles = new string[] { fileQuery.FileName };

				if (fileQuery.Command == FileQueryCommand.OpenFileInline)
				{
					if (this.IsAuthorizedInternal())
						status = this.GetSingleFile();
					else
						status = HttpStatusCode.Unauthorized;
				}
				if (status != HttpStatusCode.OK)
				{
					context.Response.Clear();
					context.Response.StatusCode = (int)status;
				}
			}
			catch (Exception ex)
			{
				#region Handle Exceptions

				try { status = OnError(ex); }
				catch { status = HttpStatusCode.InternalServerError; }

				context.Response.Clear();
				context.Response.StatusCode = (int)status;

				#endregion
			}
		}
		/// <summary>
		/// Perform the open as attachment request. This method is used by HSS.Interlink and not meant to be called from user code.
		/// </summary>
		/// <param name="context">The current HttpContext</param>
		/// <param name="fileQuery">The currently parsed QueryString</param>
		void IDownloadHandler.OpenAsAttachment(System.Web.HttpContext context, FileQuery fileQuery)
		{
			var status = HttpStatusCode.InternalServerError;
			try
			{
				this.CommonInit(context, fileQuery);
				context.Request.InputStream.Close();
				this.RequestedFiles = new string[] { fileQuery.FileName };

				if (fileQuery.Command == FileQueryCommand.OpenFileAsAttachment)
				{
					if (this.IsAuthorizedInternal())
						status = this.GetSingleFile();
					else
						status = HttpStatusCode.Unauthorized;
				}
				if (status != HttpStatusCode.OK)
				{
					context.Response.Clear();
					context.Response.StatusCode = (int)status;
				}
			}
			catch (Exception ex)
			{
				#region Handle Exceptions

				try { status = OnError(ex); }
				catch { status = HttpStatusCode.InternalServerError; }

				context.Response.Clear();
				context.Response.StatusCode = (int)status;

				#endregion
			}
		}
		#endregion

		#region IFileTransmit
		/// <summary>
		/// Writes the specified file directly to the current HTTP response output stream, without buffering it in memory.
		/// </summary>
		/// <param name="filePath">The absolute path to the file to transmit.</param>
		/// <param name="fileName">The name of the file that is being transmitted.</param>
		/// <param name="fileLength">The length of the file being sent.</param>
		/// <param name="attachment">true to send with Attachment Disposition; otherwise false for Inline Disposition.</param>
		/// <param name="contentTypeOverride">Optional parameter to specify the Content-Type for the response; otherwise an empty string to allow automatic Content-Type resolution based on file extension.</param>
		void IFileTransmit.TransmitLocalFile(string filePath, string fileName, long fileLength, bool attachment, string contentTypeOverride)
		{
			this.TransmitLocalFile(filePath, fileName, fileLength, attachment, contentTypeOverride);
		}
		#endregion

		#region Protected
		/// <summary>
		/// Writes the specified file directly to the current HTTP response output stream, without buffering it in memory.
		/// </summary>
		/// <param name="filePath">The full local path of the file to write to the HTTP output.</param>
		/// <exception cref="System.IO.FileNotFoundException">The filePath parameter provided does not exist.</exception>
		protected virtual void TransmitFile(string filePath)
		{
			var fi = new FileInfo(filePath);
			this.TransmitFile(fi, this.Query.Command == FileQueryCommand.OpenFileAsAttachment, string.Empty);
		}
		/// <summary>
		/// Writes the specified file directly to the current HTTP response output stream, without buffering it in memory.
		/// </summary>
		/// <param name="filePath">The full local path of the file to write to the HTTP output.</param>
		/// <param name="attachment">true to send with Attachment Disposition; otherwise false for Inline Disposition.</param>
		/// <param name="contentTypeOverride">Optional parameter to specify the Content-Type for the response; otherwise null or an empty string to allow automatic Content-Type resolution based on file extension.</param>
		/// <exception cref="System.IO.FileNotFoundException">The filePath parameter provided does not exist.</exception>
		protected virtual void TransmitFile(string filePath, bool attachment, string contentTypeOverride)
		{
			var fi = new FileInfo(filePath);
			this.TransmitFile(fi, attachment, contentTypeOverride);
		}
		/// <summary>
		/// Writes the specified file directly to the current HTTP response output stream, without buffering it in memory.
		/// </summary>
		/// <param name="fileInfo">A <see cref="FileInfo"/> object instance of the file to transmit.</param>
		/// <exception cref="System.IO.FileNotFoundException">The filePath parameter provided does not exist.</exception>
		protected virtual void TransmitFile(FileInfo fileInfo)
		{
			if (!fileInfo.Exists) throw new FileNotFoundException("File does not exist or invalid path provided.", fileInfo.FullName);
			this.TransmitLocalFile(fileInfo.FullName, fileInfo.Name, fileInfo.Length, this.Query.Command == FileQueryCommand.OpenFileAsAttachment, string.Empty);
		}
		/// <summary>
		/// Writes the specified file directly to the current HTTP response output stream, without buffering it in memory.
		/// </summary>
		/// <param name="fileInfo">A <see cref="FileInfo"/> object instance of the file to transmit.</param>
		/// <param name="attachment">true to send with Attachment Disposition; otherwise false for Inline Disposition.</param>
		/// <param name="contentTypeOverride">Optional parameter to specify the Content-Type for the response; otherwise null or an empty string to allow automatic Content-Type resolution based on file extension.</param>
		/// <exception cref="System.IO.FileNotFoundException">The filePath parameter provided does not exist.</exception>
		protected virtual void TransmitFile(FileInfo fileInfo, bool attachment, string contentTypeOverride)
		{
			if (!fileInfo.Exists) throw new FileNotFoundException("File does not exist or invalid path provided.", fileInfo.FullName);
			this.TransmitLocalFile(fileInfo.FullName, fileInfo.Name, fileInfo.Length, attachment, contentTypeOverride);
		}
		#endregion

		#region Private
		private void TransmitLocalFile(string filePath, string fileName, long fileLength, bool attachment = true, string contentTypeOverride = "")
		{
			try
			{
				var rs = this.Context.Response;
				{
					rs.ContentType = contentTypeOverride == "" ? MimeMapping.FromFileName(fileName) : contentTypeOverride;
					rs.AppendHeader("Content-Length", fileLength.ToString());
					rs.AppendHeader("Content-Disposition", (!attachment ? "inline" : "attachment") + "; filename=" + fileName);

					if (this.EnableClientCache)
					{
						var cachePolicy = this.Context.Response.Cache;
						cachePolicy.SetValidUntilExpires(true);
						cachePolicy.SetCacheability(this.ClientCacheability);
						cachePolicy.SetExpires(DateTime.Now + this.ClientCacheExpiration);
					}

					rs.Clear();
					rs.TransmitFile(filePath);
				}
			}
			catch (System.Web.HttpException hex)
			{
				if (!hex.Message.ToLowerInvariant().Contains("aborted"))
					throw;
			}
			catch (System.Threading.ThreadAbortException)
			{
				// If user disconnected or request was cancelled.
			}
			catch (Exception ex)
			{
				if (!ex.Message.StartsWith("The remote host closed the connection"))
					throw ex;
			}
		}
		private bool PrepareJob(string jobFile, bool single)
		{
			if (!this.SetRequestedFiles(jobFile, single))
				return false;
			if (!this.IsAuthorizedInternal())
				return false;
			return true;
		}
		private bool SetRequestedFiles(string fileName, bool single)
		{
			this.RequestedFiles = new string[] { };

			var filePath = "";
			if (!DownloadTempCache.Current.TryGetFilePath(fileName, out filePath))
			{
				this.Context.Request.InputStream.Close();
				this.Context.Response.Clear();
				this.Context.Response.StatusCode = 500;
				return false;
			}

			var requestContents = File.ReadAllText(filePath);
			var files = requestContents.Split(',');

			if (files.Length > 0 &&
				((single && files.Length == 1) || (!single && files.Length >= 1)))
			{
				this.RequestedFiles = files;
				this.Context.Request.InputStream.Close();
				return true;
			}

			this.Context.Request.InputStream.Close();
			this.Context.Response.Clear();
			this.Context.Response.StatusCode = 500;
			return false;
		}
		#endregion

		#region Abstract (API)
		/// <summary>
		/// Retrieve a single file.
		/// </summary>
		/// <returns>The desired <see cref="HttpStatusCode"/> value.</returns>
		public abstract HttpStatusCode GetSingleFile();
		/// <summary>
		/// Retrieve multiple files.
		/// </summary>
		/// <returns>The desired <see cref="HttpStatusCode"/> value.</returns>
		public abstract HttpStatusCode GetMultipleFiles();
		/// <summary>
		/// Called when an error is encountered.
		/// </summary>
		/// <param name="ex">The exception that was encountered.</param>
		/// <returns>The desired <see cref="HttpStatusCode"/> value.</returns>
		public abstract HttpStatusCode OnError(Exception ex);
		#endregion

		#endregion
	}
	#endregion
}